/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package org.unidata.mdm.dq.core.service.impl.instance;

import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.unidata.mdm.core.service.UPathService;
import org.unidata.mdm.core.type.data.ArrayAttribute;
import org.unidata.mdm.core.type.data.SimpleAttribute;
import org.unidata.mdm.core.type.model.instance.AbstractNamedDisplayableImpl;
import org.unidata.mdm.core.type.upath.UPath;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionInputParam;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionOutputParam;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionParam;
import org.unidata.mdm.dq.core.type.constant.ArrayValueConstantType;
import org.unidata.mdm.dq.core.type.constant.SingleValueConstantType;
import org.unidata.mdm.dq.core.type.model.instance.ArrayValueElement;
import org.unidata.mdm.dq.core.type.model.instance.ConstantValueElement;
import org.unidata.mdm.dq.core.type.model.instance.ConstantValueElement.InputConstantElement;
import org.unidata.mdm.dq.core.type.model.instance.ConstantValueElement.OutputConstantElement;
import org.unidata.mdm.dq.core.type.model.instance.InputMappingElement;
import org.unidata.mdm.dq.core.type.model.instance.OutputMappingElement;
import org.unidata.mdm.dq.core.type.model.instance.QualityRuleElement;
import org.unidata.mdm.dq.core.type.model.instance.RuleMappingElement;
import org.unidata.mdm.dq.core.type.model.instance.MappingSetElement;
import org.unidata.mdm.dq.core.type.model.instance.SingleValueElement;
import org.unidata.mdm.dq.core.type.model.source.constant.CleanseFunctionConstant;
import org.unidata.mdm.dq.core.type.model.source.rule.InputPortMapping;
import org.unidata.mdm.dq.core.type.model.source.rule.OutputPortMapping;
import org.unidata.mdm.dq.core.type.model.source.rule.MappingSetSource;
import org.unidata.mdm.dq.core.type.model.source.rule.RuleMappingSource;
import org.unidata.mdm.dq.core.util.DQUtils;

/**
 * @author Mikhail Mikhailov on Feb 28, 2021
 */
public class MappingSetImpl extends AbstractNamedDisplayableImpl implements MappingSetElement {
    /**
     * The source.
     */
    private final MappingSetSource source;
    /**
     * The rules.
     */
    private final Map<String, RuleMappingElement> mappings;
    /**
     * Constructor.
     * @param upathService UPS
     * @param name
     * @param displayName
     * @param description
     */
    public MappingSetImpl(UPathService upathService, DataQualityInstanceImpl dqi, MappingSetSource source) {
        super(source.getName(), source.getDisplayName(), source.getDescription());
        this.source = source;
        this.mappings = CollectionUtils.isEmpty(source.getMappings()) ? Collections.emptyMap() : new LinkedHashMap<>(source.getMappings().size());

        for (RuleMappingSource m : source.getMappings()) {
            mappings.put(m.getRuleName(), new RuleMappingImpl(upathService, dqi, m));
        }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Iterator<RuleMappingElement> iterator() {
        return mappings.values().iterator();
    }
    /**
     * Required for model assembling.
     * @return source
     */
    public MappingSetSource getSource() {
        return source;
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getId() {
        return getName();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public OffsetDateTime getCreateDate() {
        return source.getCreateDate();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getCreatedBy() {
        return source.getCreatedBy();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public OffsetDateTime getUpdateDate() {
        return source.getUpdateDate();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public String getUpdatedBy() {
        return source.getUpdatedBy();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<RuleMappingElement> getMappings() {
        return mappings.values();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public RuleMappingElement getMapping(String name) {
        return mappings.get(name);
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasMapping(String name) {
        return mappings.containsKey(name);
    }
    /**
     * @author Mikhail Mikhailov on Mar 5, 2021
     * The rule mapping.
     */
    private static class RuleMappingImpl implements RuleMappingElement {
        /**
         * Input mapping.
         */
        private final Map<String, InputMappingElement> input;
        /**
         * Output mapping.
         */
        private final Map<String, OutputMappingElement> output;
        /**
         * Compiled root path.
         */
        private final UPath localPath;
        /**
         * The rule.
         */
        private final QualityRuleElement rule;
        /**
         * Constructor.
         * @param dqi
         * @param source
         */
        public RuleMappingImpl(UPathService upathService, DataQualityInstanceImpl dqi, RuleMappingSource source) {
            super();
            this.input = source.getInputMappings().stream().map(m -> new InputMappingImpl(m, upathService)).collect(Collectors.toMap(InputMappingElement::getPortName, Function.identity()));
            this.output = source.getOutputMappings().stream().map(m -> new OutputMappingImpl(m, upathService)).collect(Collectors.toMap(OutputMappingElement::getPortName, Function.identity()));
            this.localPath = StringUtils.isNotBlank(source.getLocalPath()) ? upathService.upathCreate(source.getLocalPath()) : null;
            this.rule = dqi.getRule(source.getRuleName());
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public QualityRuleElement getRule() {
            return rule;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public Collection<String> getInputMappingPorts() {
            return input.keySet();
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public Collection<InputMappingElement> getInputMappings() {
            return input.values();
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public InputMappingElement getInputMapping(String port) {
            return input.get(port);
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public Collection<String> getOutputMappingPorts() {
            return output.keySet();
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public Collection<OutputMappingElement> getOutputMappings() {
            return output.values();
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public OutputMappingElement getOutputMapping(String port) {
            return output.get(port);
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public UPath getLocalPath() {
            return localPath;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isLocal() {
            return Objects.nonNull(localPath);
        }
    }
    /**
     * @author Mikhail Mikhailov on Feb 28, 2021
     * Input mapping.
     */
    private static class InputMappingImpl implements InputMappingElement {
        /**
         * Port name.
         */
        private final String portName;
        /**
         * Path.
         */
        private final UPath path;
        /**
         * Constant.
         */
        private final ConstantValueElement constant;
        /**
         * Constructor.
         * @param source the source
         * @param ups the service
         */
        public InputMappingImpl(InputPortMapping source, UPathService ups) {
            super();
            this.portName = source.getPortName();
            this.path = StringUtils.isNotBlank(source.getInputPath()) ? ups.upathCreate(source.getInputPath()) : null;

            if (Objects.nonNull(source.getConstantValue())) {
                this.constant = source.getConstantValue().isSingle()
                        ? new SingleConstantImpl(source.getConstantValue(), source.getPortName(), true)
                        : new ArrayConstantImpl(source.getConstantValue(), source.getPortName(), true);
            } else {
                this.constant = null;
            }
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public String getPortName() {
            return portName;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isPath() {
            return Objects.nonNull(path);
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public UPath getInputPath() {
            return path;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isConstant() {
            return Objects.nonNull(constant);
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public ConstantValueElement getConstant() {
            return constant;
        }
    }
    /**
     * @author Mikhail Mikhailov on Feb 28, 2021
     * Output constant.
     */
    private static class OutputMappingImpl implements OutputMappingElement {
        /**
         * Port name.
         */
        private final String portName;
        /**
         * Path.
         */
        private final UPath path;
        /**
         * Constant.
         */
        private final ConstantValueElement constant;
        /**
         * Constructor.
         * @param source the source
         * @param ups the service
         */
        public OutputMappingImpl(OutputPortMapping source, UPathService ups) {
            super();
            this.portName = source.getPortName();
            this.path = StringUtils.isNotBlank(source.getOutputPath()) ? ups.upathCreate(source.getOutputPath()) : null;

            if (Objects.nonNull(source.getConstantValue())) {
                this.constant = source.getConstantValue().isSingle()
                        ? new SingleConstantImpl(source.getConstantValue(), source.getPortName(), false)
                        : new ArrayConstantImpl(source.getConstantValue(), source.getPortName(), false);
            } else {
                this.constant = null;
            }
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public String getPortName() {
            return portName;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isPath() {
            return Objects.nonNull(path);
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public UPath getOutputPath() {
            return path;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isConstant() {
            return Objects.nonNull(constant);
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public ConstantValueElement getConstant() {
            return constant;
        }
    }
    /**
     * @author Mikhail Mikhailov on Feb 28, 2021
     * Array constant.
     */
    private static class ArrayConstantImpl implements ConstantValueElement, ArrayValueElement, InputConstantElement, OutputConstantElement {
        /**
         * Prepared param value.
         */
        private final CleanseFunctionParam param;
        /**
         * Direction.
         */
        private final boolean input;
        /**
         * Constructor.
         * @param constant the input
         */
        public ArrayConstantImpl(CleanseFunctionConstant constant, String portName, boolean input) {
            super();
            this.input = input;
            this.param = DQUtils.ofConstant(constant, portName, input);
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public ArrayValueConstantType getType() {
            ArrayAttribute<?> value = param.getSingleton();
            return ArrayValueConstantType.fromDataType(value.getDataType());
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public <V> List<V> getValues() {
            V[] payload = param.toValues();
            return ArrayUtils.isEmpty(payload) ? Collections.emptyList() : Arrays.asList(payload);
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isArray() {
            return true;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public ArrayValueElement getArray() {
            return this;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public CleanseFunctionOutputParam toOutput() {
            return input ? null : (CleanseFunctionOutputParam) param;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public CleanseFunctionInputParam toInput() {
            return input ? (CleanseFunctionInputParam) param : null;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isInput() {
            return input;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public InputConstantElement getInput() {
            return input ? this : null;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isOutput() {
            return !input;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public OutputConstantElement getOutput() {
            return input ? null : this;
        }
    }
    /**
     * @author Mikhail Mikhailov on Feb 28, 2021
     * Single constant.
     */
    private static class SingleConstantImpl implements ConstantValueElement, SingleValueElement, InputConstantElement, OutputConstantElement {
        /**
         * Prepared param value.
         */
        private final CleanseFunctionParam param;
        /**
         * Direction.
         */
        private final boolean input;
        /**
         * Constructor.
         * @param constant the input
         * @param portName the name of the port.
         */
        public SingleConstantImpl(CleanseFunctionConstant constant, String portName, boolean input) {
            super();
            this.input = input;
            this.param = DQUtils.ofConstant(constant, portName, input);
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public SingleValueConstantType getType() {
            SimpleAttribute<?> value = param.getSingleton();
            return SingleValueConstantType.fromDataType(value.getDataType());
        }
        /**
         * {@inheritDoc}
         */
        @SuppressWarnings("unchecked")
        @Override
        public <V> V getValue() {
            return (V) param.toSingletonValue();
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isSingle() {
            return true;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public SingleValueElement getSingle() {
            return this;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isInput() {
            return input;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public InputConstantElement getInput() {
            return input ? this : null;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public boolean isOutput() {
            return !input;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public OutputConstantElement getOutput() {
            return input ? null : this;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public CleanseFunctionOutputParam toOutput() {
            return input ? null : (CleanseFunctionOutputParam) param;
        }
        /**
         * {@inheritDoc}
         */
        @Override
        public CleanseFunctionInputParam toInput() {
            return input ? (CleanseFunctionInputParam) param : null;
        }
    }
}
